추가로 알아야 할 것들 — dmesg·siginfo_t·ASan·예방 도구

NOTE

core dump 분석은 “이미 터진 뒤”의 사후 부검이다. 여기서는 core dump를 보완하거나, 애초에 프로덕션에서 터지기 전에 잡아내는 방법들을 정리한다.

1. 커널 로그(dmesg/journalctl)도 함께 확인한다

SIGSEGV가 나면 커널이 자체적으로 syslog에 한 줄 남긴다. core dump 설정이 꺼져 있어도 이 로그는 대부분 남기 때문에 1차 단서로 가장 빠르다.

dmesg -T | grep -i segfault
# 또는
journalctl -k | grep -i segfault

출력 예:

myprog[12345]: segfault at 0 ip 0000555555555169 sp 00007fffffffe410 error 4 in myprog[555555554000+1000]
필드의미
at 0접근하려던(폴트난) 메모리 주소 — 0이면 NULL 포인터 역참조 확정
ip크래시 시점의 명령어 포인터 (어느 명령어에서 터졌는지)
sp크래시 시점의 스택 포인터
error 4비트마스크 — 1:쓰기(0=읽기), 2:유저모드(0=커널), 4:페이지 없음(0=권한문제) 등의 조합. error 4=유저모드에서 매핑 안 된 페이지 읽기

ip 값을 addr2line -e myprog -f -C <ip 주소>에 넣으면 core dump 없이도 대략적인 함수/라인을 뽑을 수 있다(빌드 시 -g 필요).

TIP

dmesg | grep -i "killed process"로 OOM killer 로그도 같이 확인한다. SIGKILL(137)로 죽었는데 코드에 원인이 안 보인다면 크래시가 아니라 메모리 부족으로 강제 종료된 것일 수 있다 — 1. 비정상 종료 시그널 종류의 $? 표 참고.

2. 죽기 직전에 스스로 스택을 찍게 만들기 — sigaction + backtrace()

운영 환경에서 core dump 저장 공간이 없거나 정책상 못 남기는 경우, 크래시 시그널 핸들러를 직접 등록해서 죽기 직전에 스택 트레이스를 로그로 남기는 패턴이 실무에서 많이 쓰인다.

#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
 
static void crash_handler(int sig, siginfo_t *info, void *ucontext)
{
    void *frames[32];
    int n = backtrace(frames, 32);
 
    dprintf(STDERR_FILENO, "signal %d, faulting address %p\n", sig, info->si_addr);
    backtrace_symbols_fd(frames, n, STDERR_FILENO);  /* async-signal-safe */
 
    signal(sig, SIG_DFL);   /* 기본 동작 복원 후 재발생 → 정상적으로 core도 남김 */
    raise(sig);
}
 
void install_crash_handler(void)
{
    struct sigaction sa = {0};
    sa.sa_sigaction = crash_handler;
    sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);
    sigaction(SIGILL,  &sa, NULL);
    sigaction(SIGFPE,  &sa, NULL);
}

WARNING

크래시 핸들러 안에서는 async-signal-safe 함수만 호출 가능하다 — malloc/free/printf는 내부적으로 락을 잡기 때문에, 크래시 시점에 마침 그 락을 들고 있었다면 핸들러 안에서 데드락에 빠진다. backtrace_symbols_fd(내부에서 malloc 안 함)와 dprintf/write처럼 안전한 함수만 쓴다. 이 원칙은 [C] 실무 C 코드 관례와 UB 함정 정리의 시그널 핸들러 관례와 동일한 맥락이다.

3. 터지기 전에 잡는다 — Sanitizer / Valgrind

Core dump·gdb는 이미 터진 뒤의 사후 분석이라 정확한 순간을 못 잡을 때가 많다(특히 힙 손상은 “망가뜨린 시점”과 “터지는 시점”이 다르다). 개발/테스트 단계에서 아래 도구를 상시로 돌리면 SIGABRT/SIGSEGV로 가기 전에 정확한 원인 라인을 알려준다.

도구빌드 방법잡아내는 것
AddressSanitizer(ASan)gcc -fsanitize=address -g버퍼 오버플로우, Use-After-Free, Double Free — 정확한 발생 라인을 즉시 리포트
UndefinedBehaviorSanitizer(UBSan)gcc -fsanitize=undefined -g정수 오버플로우, 정렬 위반, NULL에 대한 산술 등 UB
Valgrind (memcheck)별도 빌드 불필요, valgrind ./a.out초기화 안 된 값 사용, 메모리 누수, 잘못된 free — ASan보다 느리지만 소스 재컴파일 없이 사용 가능
gdb watch—특정 메모리 주소가 “언제, 어느 코드에서” 바뀌는지 실시간 추적 (힙 손상 범위 좁히기)

재현이 간헐적인 크래시(레이스 컨디션, UAF)는 ASan을 켠 빌드로 스트레스 테스트를 돌리는 게 gdb로 core를 백날 들여다보는 것보다 빠르다.

4. 컴파일러/런타임 하드닝 옵션 — 예방적 방어

옵션효과
-fstack-protector-strong스택 버퍼 오버플로우 시 canary로 감지 → SIGSEGV 대신 명확한 SIGABRT(stack smashing detected)로 전환
-D_FORTIFY_SOURCE=2 (+-O1 이상)strcpy/sprintf/memcpy 등을 버퍼 크기 검사가 들어간 __*_chk 버전으로 치환
-fsanitize=address위 ASan (개발/테스트 빌드 전용, 런타임 오버헤드 큼 — 프로덕션에는 보통 비활성)

5. 컨테이너 환경의 함정

  • core_pattern은 호스트 커널의 전역 설정이라 컨테이너 안에서 ulimit -c unlimited만 해서는 부족한 경우가 있다 — 파이프(|systemd-coredump...) 패턴이면 호스트 저널로 가고, 컨테이너 안에서는 안 보인다.
  • 컨테이너에 --cap-drop이나 seccomp 프로파일이 걸려 있으면 SIGSYS가 늘어난다 — 호스트에서는 멀쩡하던 코드가 컨테이너에서만 죽으면 seccomp부터 의심한다.
  • core dump를 컨테이너 안에 남기려면 볼륨 마운트 + core_pattern을 컨테이너에 맞게(상대경로, 절대경로 파이프 회피) 조정해야 한다.

6. 체크리스트 — 크래시 리포트 받았을 때 순서

  1. $? 또는 로그의 종료 코드로 어떤 시그널인지 역산 (128+N)
  2. dmesg/journalctl에서 커널 로그 확인 (주소, error 비트)
  3. core dump가 있는지 확인 (coredumpctl list 또는 core_pattern 경로)
  4. core가 없으면 재현 시도 + ulimit -c unlimited로 core 생성을 유도하거나, ASan 빌드로 재현
  5. gdb로 bt full → 크래시 지점의 변수 상태 확인
  6. 원인 패턴을 2. 시그널별 원인 분석의 표와 대조
  7. 수정 후 회귀 방지: 같은 입력으로 ASan/UBSan 빌드 테스트를 CI에 추가

관련 문서